<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
  <title></title>
  <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css" />
  
<style type="text/css">
@page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }
</style>
</head>

<body>
  <h2><span style="border-bottom:1px solid">Chapter_15</span></h2>

  <p>m.in是一个原始的InputStream，未进行任何封装处理。这意味着尽管能直接使用System.out和System.err，但必须事先封装System.in，否则不能从中读取数据。</p>

  <p>典型情况下，我们希望用readLine()每次读取一行输入信息，所以需要将System.in封装到一个DataInputStream中。这是Java 1.0进行行输入时采取的“老”办法。在本章稍后，大家还会看到Java 1.1的解决方案。下面是个简单的例子，作用是回应我们键入的每一行内容：</p>

  <p>//: Echo.java</p>

  <p>// How to read from standard input</p>

  <p>import java.io.*;</p>

  <p>public class Echo {</p>

  <p>public static void main(String[] args) {</p>

  <p>DataInputStream in =</p>

  <p>new DataInputStream(</p>

  <p>new BufferedInputStream(System.in));</p>

  <p>String s;</p>

  <p>try {</p>

  <p>while((s = in.readLine()).length() != 0)</p>

  <p>System.out.println(s);</p>

  <p>// An empty line terminates the program</p>

  <p>} catch(IOException e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>之所以要使用try块，是由于readLine()可能“掷”出一个IOException。注意同其他大多数流一样，也应对System.in进行缓冲。</p>

  <p>由于在每个程序中都要将System.in封装到一个DataInputStream内，所以显得有点不方便。但采用这种设计方案，可以获得最大的灵活性。</p>

  <p>10.5.5 管道数据流</p>

  <p>本章已简要介绍了PipedInputStream（管道输入流）和PipedOutputStream（管道输出流）。尽管描述不十分详细，但并不是说它们作用不大。然而，只有在掌握了多线程处理的概念后，才可真正体会它们的价值所在。原因很简单，因为管道化的数据流就是用于线程之间的通信。这方面的问题将在第14章用一个示例说明。</p>

  <p>10.6 StreamTokenizer</p>

  <p>尽管StreamTokenizer并不是从InputStream或OutputStream衍生的，但它只随同InputStream工作，所以十分恰当地包括在库的IO部分中。</p>

  <p>StreamTokenizer类用于将任何InputStream分割为一系列“记号”（Token）。这些记号实际是一些断续的文本块，中间用我们选择的任何东西分隔。例如，我们的记号可以是单词，中间用空白（空格）以及标点符号分隔。</p>

  <p>下面是一个简单的程序，用于计算各个单词在文本文件中重复出现的次数：</p>

  <p>//: SortedWordCount.java</p>

  <p>// Counts words in a file, outputs</p>

  <p>// results in sorted form.</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>import c08.*; // Contains StrSortVector</p>

  <p>class Counter {</p>

  <p>private int i = 1;</p>

  <p>int read() { return i; }</p>

  <p>void increment() { i++; }</p>

  <p>}</p>

  <p>public class SortedWordCount {</p>

  <p>private FileInputStream file;</p>

  <p>private StreamTokenizer st;</p>

  <p>private Hashtable counts = new Hashtable();</p>

  <p>SortedWordCount(String filename)</p>

  <p>throws FileNotFoundException {</p>

  <p>try {</p>

  <p>file = new FileInputStream(filename);</p>

  <p>st = new StreamTokenizer(file);</p>

  <p>st.ordinaryChar('.');</p>

  <p>st.ordinaryChar('-');</p>

  <p>} catch(FileNotFoundException e) {</p>

  <p>System.out.println(</p>

  <p>"Could not open " + filename);</p>

  <p>throw e;</p>

  <p>}</p>

  <p>}</p>

  <p>void cleanup() {</p>

  <p>try {</p>

  <p>file.close();</p>

  <p>} catch(IOException e) {</p>

  <p>System.out.println(</p>

  <p>"file.close() unsuccessful");</p>

  <p>}</p>

  <p>}</p>

  <p>void countWords() {</p>

  <p>try {</p>

  <p>while(st.nextToken() !=</p>

  <p>StreamTokenizer.TT_EOF) {</p>

  <p>String s;</p>

  <p>switch(st.ttype) {</p>

  <p>case StreamTokenizer.TT_EOL:</p>

  <p>s = new String("EOL");</p>

  <p>break;</p>

  <p>case StreamTokenizer.TT_NUMBER:</p>

  <p>s = Double.toString(st.nval);</p>

  <p>break;</p>

  <p>case StreamTokenizer.TT_WORD:</p>

  <p>s = st.sval; // Already a String</p>

  <p>break;</p>

  <p>default: // single character in ttype</p>

  <p>s = String.valueOf((char)st.ttype);</p>

  <p>}</p>

  <p>if(counts.containsKey(s))</p>

  <p>((Counter)counts.get(s)).increment();</p>

  <p>else</p>

  <p>counts.put(s, new Counter());</p>

  <p>}</p>

  <p>} catch(IOException e) {</p>

  <p>System.out.println(</p>

  <p>"st.nextToken() unsuccessful");</p>

  <p>}</p>

  <p>}</p>

  <p>Enumeration values() {</p>

  <p>return counts.elements();</p>

  <p>}</p>

  <p>Enumeration keys() { return counts.keys(); }</p>

  <p>Counter getCounter(String s) {</p>

  <p>return (Counter)counts.get(s);</p>

  <p>}</p>

  <p>Enumeration sortedKeys() {</p>

  <p>Enumeration e = counts.keys();</p>

  <p>StrSortVector sv = new StrSortVector();</p>

  <p>while(e.hasMoreElements())</p>

  <p>sv.addElement((String)e.nextElement());</p>

  <p>// This call forces a sort:</p>

  <p>return sv.elements();</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>try {</p>

  <p>SortedWordCount wc =</p>

  <p>new SortedWordCount(args[0]);</p>

  <p>wc.countWords();</p>

  <p>Enumeration keys = wc.sortedKeys();</p>

  <p>while(keys.hasMoreElements()) {</p>

  <p>String key = (String)keys.nextElement();</p>

  <p>System.out.println(key + ": "</p>

  <p>+ wc.getCounter(key).read());</p>

  <p>}</p>

  <p>wc.cleanup();</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>最好将结果按排序格式输出，但由于Java 1.0和Java 1.1都没有提供任何排序方法，所以必须由自己动手。这个目标可用一个StrSortVector方便地达成（创建于第8章，属于那一章创建的软件包的一部分。记住本书所有子目录的起始目录都必须位于类路径中，否则程序将不能正确地编译）。</p>

  <p>为打开文件，使用了一个FileInputStream。而且为了将文件转换成单词，从FileInputStream中创建了一个StreamTokenizer。在StreamTokenizer中，存在一个默认的分隔符列表，我们可用一系列方法加入更多的分隔符。在这里，我们用ordinaryChar()指出“该字符没有特别重要的意义”，所以解析器不会把它当作自己创建的任何单词的一部分。例如，st.ordinaryChar('.')表示小数点不会成为解析出来的单词的一部分。在与Java配套提供的联机文档中，可以找到更多的相关信息。</p>

  <p>在countWords()中，每次从数据流中取出一个记号，而ttype信息的作用是判断对每个记号采取什么操作――因为记号可能代表一个行尾、一个数字、一个字串或者一个字符。</p>

  <p>找到一个记号后，会查询Hashtable counts，核实其中是否已经以“键”（Key）的形式包含了一个记号。若答案是肯定的，对应的Counter（计数器）对象就会增值，指出已找到该单词的另一个实例。若答案为否，则新建一个Counter――因为Counter构建器会将它的值初始化为1，正是我们计算单词数量时的要求。</p>

  <p>SortedWordCount并不属于Hashtable（散列表）的一种类型，所以它不会继承。它执行的一种特定类型的操作，所以尽管keys()和values()方法都必须重新揭示出来，但仍不表示应使用那个继承，因为大量Hashtable方法在这里都是不适当的。除此以外，对于另一些方法来说（比如getCounter()――用于获得一个特定字串的计数器；又如sortedKeys()――用于产生一个枚举），它们最终都改变了SortedWordCount接口的形式。</p>

  <p>在main()内，我们用SortedWordCount打开和计算文件中的单词数量――总共只用了两行代码。随后，我们为一个排好序的键（单词）列表提取出一个枚举。并用它获得每个键以及相关的Count（计数）。注意必须调用cleanup()，否则文件不能正常关闭。</p>

  <p>采用了StreamTokenizer的第二个例子将在第17章提供。</p>

  <p>10.6.1 StringTokenizer</p>

  <p>尽管并不必要IO库的一部分，但StringTokenizer提供了与StreamTokenizer极相似的功能，所以在这里一并讲述。</p>

  <p>StringTokenizer的作用是每次返回字串内的一个记号。这些记号是一些由制表站、空格以及新行分隔的连续字符。因此，字串“Where is my cat?”的记号分别是“Where”、“is”、“my”和“cat?”。与StreamTokenizer类似，我们可以指示StringTokenizer按照我们的愿望分割输入。但对于StringTokenizer，却需要向构建器传递另一个参数，即我们想使用的分隔字串。通常，如果想进行更复杂的操作，应使用StreamTokenizer。</p>

  <p>可用nextToken()向StringTokenizer对象请求字串内的下一个记号。该方法要么返回一个记号，要么返回一个空字串（表示没有记号剩下）。</p>

  <p>作为一个例子，下述程序将执行一个有限的句法分析，查询键短语序列，了解句子暗示的是快乐亦或悲伤的含义。</p>

  <p>//: AnalyzeSentence.java</p>

  <p>// Look for particular sequences</p>

  <p>// within sentences.</p>

  <p>import java.util.*;</p>

  <p>public class AnalyzeSentence {</p>

  <p>public static void main(String[] args) {</p>

  <p>analyze("I am happy about this");</p>

  <p>analyze("I am not happy about this");</p>

  <p>analyze("I am not! I am happy");</p>

  <p>analyze("I am sad about this");</p>

  <p>analyze("I am not sad about this");</p>

  <p>analyze("I am not! I am sad");</p>

  <p>analyze("Are you happy about this?");</p>

  <p>analyze("Are you sad about this?");</p>

  <p>analyze("It's you! I am happy");</p>

  <p>analyze("It's you! I am sad");</p>

  <p>}</p>

  <p>static StringTokenizer st;</p>

  <p>static void analyze(String s) {</p>

  <p>prt("\nnew sentence &gt;&gt; " + s);</p>

  <p>boolean sad = false;</p>

  <p>st = new StringTokenizer(s);</p>

  <p>while (st.hasMoreTokens()) {</p>

  <p>String token = next();</p>

  <p>// Look until you find one of the</p>

  <p>// two starting tokens:</p>

  <p>if(!token.equals("I") &amp;&amp;</p>

  <p>!token.equals("Are"))</p>

  <p>continue; // Top of while loop</p>

  <p>if(token.equals("I")) {</p>

  <p>String tk2 = next();</p>

  <p>if(!tk2.equals("am")) // Must be after I</p>

  <p>break; // Out of while loop</p>

  <p>else {</p>

  <p>String tk3 = next();</p>

  <p>if(tk3.equals("sad")) {</p>

  <p>sad = true;</p>

  <p>break; // Out of while loop</p>

  <p>}</p>

  <p>if (tk3.equals("not")) {</p>

  <p>String tk4 = next();</p>

  <p>if(tk4.equals("sad"))</p>

  <p>break; // Leave sad false</p>

  <p>if(tk4.equals("happy")) {</p>

  <p>sad = true;</p>

  <p>break;</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>}</p>

  <p>if(token.equals("Are")) {</p>

  <p>String tk2 = next();</p>

  <p>if(!tk2.equals("you"))</p>

  <p>break; // Must be after Are</p>

  <p>String tk3 = next();</p>

  <p>if(tk3.equals("sad"))</p>

  <p>sad = true;</p>

  <p>break; // Out of while loop</p>

  <p>}</p>

  <p>}</p>

  <p>if(sad) prt("Sad detected");</p>

  <p>}</p>

  <p>static String next() {</p>

  <p>if(st.hasMoreTokens()) {</p>

  <p>String s = st.nextToken();</p>

  <p>prt(s);</p>

  <p>return s;</p>

  <p>}</p>

  <p>else</p>

  <p>return "";</p>

  <p>}</p>

  <p>static void prt(String s) {</p>

  <p>System.out.println(s);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>对于准备分析的每个字串，我们进入一个while循环，并将记号从那个字串中取出。请注意第一个if语句，假如记号既不是“I”，也不是“Are”，就会执行continue（返回循环起点，再一次开始）。这意味着除非发现一个“I”或者“Are”，才会真正得到记号。大家可能想用==代替equals()方法，但那样做会出现不正常的表现，因为==比较的是句柄值，而equals()比较的是内容。</p>

  <p>analyze()方法剩余部分的逻辑是搜索“I am sad”（我很忧伤、“I am nothappy”（我不快乐）或者“Are you sad?”（你悲伤吗？）这样的句法格式。若没有break语句，这方面的代码甚至可能更加散乱。大家应注意对一个典型的解析器来说，通常都有这些记号的一个表格，并能在读取新记号的时候用一小段代码在表格内移动。</p>

  <p>无论如何，只应将StringTokenizer看作StreamTokenizer一种简单而且特殊的简化形式。然而，如果有一个字串需要进行记号处理，而且StringTokenizer的功能实在有限，那么应该做的全部事情就是用StringBufferInputStream将其转换到一个数据流里，再用它创建一个功能更强大的StreamTokenizer。</p>

  <p>10.7 Java 1.1的IO流</p>

  <p>到这个时候，大家或许会陷入一种困境之中，怀疑是否存在IO流的另一种设计方案，并可能要求更大的代码量。还有人能提出一种更古怪的设计吗？事实上，Java 1.1对IO流库进行了一些重大的改进。看到Reader和Writer类时，大多数人的第一个印象（就象我一样）就是它们用来替换原来的InputStream和OutputStream类。但实情并非如此。尽管不建议使用原始数据流库的某些功能（如使用它们，会从编译器收到一条警告消息），但原来的数据流依然得到了保留，以便维持向后兼容，而且：</p>

  <p>(1) 在老式层次结构里加入了新类，所以Sun公司明显不会放弃老式数据流。</p>

  <p>(2) 在许多情况下，我们需要与新结构中的类联合使用老结构中的类。为达到这个目的，需要使用一些“桥”类：InputStreamReader将一个InputStream转换成Reader，OutputStreamWriter将一个OutputStream转换成Writer。</p>

  <p>所以与原来的IO流库相比，经常都要对新IO流进行层次更多的封装。同样地，这也属于装饰器方案的一个缺点――需要为额外的灵活性付出代价。</p>

  <p>之所以在Java 1.1里添加了Reader和Writer层次，最重要的原因便是国际化的需求。老式IO流层次结构只支持8位字节流，不能很好地控制16位Unicode字符。由于Unicode主要面向的是国际化支持（Java内含的char是16位的Unicode），所以添加了Reader和Writer层次，以提供对所有IO操作中的Unicode的支持。除此之外，新库也对速度进行了优化，可比旧库更快地运行。</p>

  <p>与本书其他地方一样，我会试着提供对类的一个概述，但假定你会利用联机文档搞定所有的细节，比如方法的详尽列表等。</p>

  <p>10.7.1 数据的发起与接收</p>

  <p>Java 1.0的几乎所有IO流类都有对应的Java 1.1类，用于提供内建的Unicode管理。似乎最容易的事情就是“全部使用新类，再也不要用旧的”，但实际情况并没有这么简单。有些时候，由于受到库设计的一些限制，我们不得不使用Java 1.0的IO流类。特别要指出的是，在旧流库的基础上新加了java.util.zip库，它们依赖旧的流组件。所以最明智的做法是“尝试性”地使用Reader和Writer类。若代码不能通过编译，便知道必须换回老式库。</p>

  <p>下面这张表格分旧库与新库分别总结了信息发起与接收之间的对应关系。</p>

  <p>Sources &amp; Sinks:</p>

  <p>Java 1.0 class</p>

  <p>Corresponding Java 1.1 class</p>

  <p>InputStream</p>

  <p>Reader</p>

  <p>converter: InputStreamReader</p>

  <p>OutputStream</p>

  <p>Writer</p>

  <p>converter: OutputStreamWriter</p>

  <p>FileInputStream</p>

  <p>FileReader</p>

  <p>FileOutputStream</p>

  <p>FileWriter</p>

  <p>StringBufferInputStream</p>

  <p>StringReader</p>

  <p>(no corresponding class)</p>

  <p>StringWriter</p>

  <p>ByteArrayInputStream</p>

  <p>CharArrayReader</p>

  <p>ByteArrayOutputStream</p>

  <p>CharArrayWriter</p>

  <p>PipedInputStream</p>

  <p>PipedReader</p>

  <p>PipedOutputStream</p>

  <p>PipedWriter我们发现即使不完全一致，但旧库组件中的接口与新接口通常也是类似的。</p>

  <p>10.7.2 修改数据流的行为</p>

  <p>在Java 1.0中，数据流通过FilterInputStream和FilterOutputStream的“装饰器”（Decorator）子类适应特定的需求。Java 1.1的IO流沿用了这一思想，但没有继续采用所有装饰器都从相同“filter”（过滤器）基础类中衍生这一做法。若通过观察类的层次结构来理解它，这可能令人出现少许的困惑。</p>

  <p>在下面这张表格中，对应关系比上一张表要粗糙一些。之所以会出现这个差别，是由类的组织造成的：尽管BufferedOutputStream是FilterOutputStream的一个子类，但是BufferedWriter并不是FilterWriter的子类（对后者来说，尽管它是一个抽象类，但没有自己的子类或者近似子类的东西，也没有一个“占位符”可用，所以不必费心地寻找）。然而，两个类的接口是非常相似的，而且不管在什么情况下，显然应该尽可能地使用新版本，而不应考虑旧版本（也就是说，除非在一些类中必须生成一个Stream，不可生成Reader或者Writer）。</p>

  <p>Filters:</p>

  <p>Java 1.0 class</p>

  <p>Corresponding Java 1.1 class</p>

  <p>FilterInputStream</p>

  <p>FilterReader</p>

  <p>FilterOutputStream</p>

  <p>FilterWriter (abstract class with no subclasses)</p>

  <p>BufferedInputStream</p>

  <p>BufferedReader</p>

  <p>(also has readLine( ))</p>

  <p>BufferedOutputStream</p>

  <p>BufferedWriter</p>

  <p>DataInputStream</p>

  <p>use DataInputStream</p>

  <p>(Except when you need to use readLine( ), when you should use a BufferedReader)</p>

  <p>PrintStream</p>

  <p>PrintWriter</p>

  <p>LineNumberInputStream</p>

  <p>LineNumberReader</p>

  <p>StreamTokenizer</p>

  <p>StreamTokenizer</p>

  <p>(use constructor that takes a Reader instead)</p>

  <p>PushBackInputStream</p>

  <p>PushBackReader过滤器：Java 1.0类 对应的Java 1.1类</p>

  <p>FilterInputStream FilterReader</p>

  <p>FilterOutputStream FilterWriter（没有子类的抽象类）</p>

  <p>BufferedInputStream BufferedReader（也有readLine()）</p>

  <p>BufferedOutputStream BufferedWriter</p>

  <p>DataInputStream 使用DataInputStream（除非要使用readLine()，那时需要使用一个BufferedReader）</p>

  <p>PrintStream PrintWriter</p>

  <p>LineNumberInputStream LineNumberReader</p>

  <p>StreamTokenizer StreamTokenizer（用构建器取代Reader）</p>

  <p>PushBackInputStream PushBackReader</p>

  <p>有一条规律是显然的：若想使用readLine()，就不要再用一个DataInputStream来实现（否则会在编译期得到一条出错消息），而应使用一个BufferedReader。但除这种情况以外，DataInputStream仍是Java 1.1 IO库的“首选”成员。</p>

  <p>为了将向PrintWriter的过渡变得更加自然，它提供了能采用任何OutputStream对象的构建器。PrintWriter提供的格式化支持没有PrintStream那么多；但接口几乎是相同的。</p>

  <p>10.7.3 未改变的类</p>

  <p>显然，Java库的设计人员觉得以前的一些类毫无问题，所以没有对它们作任何修改，可象以前那样继续使用它们：</p>

  <p>没有对应Java 1.1类的Java 1.0类</p>

  <p>DataOutputStream</p>

  <p>File</p>

  <p>RandomAccessFile</p>

  <p>SequenceInputStream</p>

  <p>特别未加改动的是DataOutputStream，所以为了用一种可转移的格式保存和获取数据，必须沿用InputStream和OutputStream层次结构。</p>

  <p>10.7.4 一个例子</p>

  <p>为体验新类的效果，下面让我们看看如何修改IOStreamDemo.java示例的相应区域，以便使用Reader和Writer类：</p>

  <p>//: NewIODemo.java</p>

  <p>// Java 1.1 IO typical usage</p>

  <p>import java.io.*;</p>

  <p>public class NewIODemo {</p>

  <p>public static void main(String[] args) {</p>

  <p>try {</p>

  <p>// 1. Reading input by lines:</p>

  <p>BufferedReader in =</p>

  <p>new BufferedReader(</p>

  <p>new FileReader(args[0]));</p>

  <p>String s, s2 = new String();</p>

  <p>while((s = in.readLine())!= null)</p>

  <p>s2 += s + "\n";</p>

  <p>in.close();</p>

  <p>// 1b. Reading standard input:</p>

  <p>BufferedReader stdin =</p>

  <p>new BufferedReader(</p>

  <p>new InputStreamReader(System.in));</p>

  <p>System.out.print("Enter a line:");</p>

  <p>System.out.println(stdin.readLine());</p>

  <p>// 2. Input from memory</p>

  <p>StringReader in2 = new StringReader(s2);</p>

  <p>int c;</p>

  <p>while((c = in2.read()) != -1)</p>

  <p>System.out.print((char)c);</p>

  <p>// 3. Formatted memory input</p>

  <p>try {</p>

  <p>DataInputStream in3 =</p>

  <p>new DataInputStream(</p>

  <p>// Oops: must use deprecated class:</p>

  <p>new StringBufferInputStream(s2));</p>

  <p>while(true)</p>

  <p>System.out.print((char)in3.readByte());</p>

  <p>} catch(EOFException e) {</p>

  <p>System.out.println("End of stream");</p>

  <p>}</p>

  <p>// 4. Line numbering &amp; file output</p>

  <p>try {</p>

  <p>LineNumberReader li =</p>

  <p>new LineNumberReader(</p>

  <p>new StringReader(s2));</p>

  <p>BufferedReader in4 =</p>

  <p>new BufferedReader(li);</p>

  <p>PrintWriter out1 =</p>

  <p>new PrintWriter(</p>

  <p>new BufferedWriter(</p>

  <p>new FileWriter("IODemo.out")));</p>

  <p>while((s = in4.readLine()) != null )</p>

  <p>out1.println(</p>

  <p>"Line " + li.getLineNumber() + s);</p>

  <p>out1.close();</p>

  <p>} catch(EOFException e) {</p>

  <p>System.out.println("End of stream");</p>

  <p>}</p>

  <p>// 5. Storing &amp; recovering data</p>

  <p>try {</p>

  <p>DataOutputStream out2 =</p>

  <p>new DataOutputStream(</p>

  <p>new BufferedOutputStream(</p>

  <p>new FileOutputStream("Data.txt")));</p>

  <p>out2.writeDouble(3.14159);</p>

  <p>out2.writeBytes("That was pi");</p>

  <p>out2.close();</p>

  <p>DataInputStream in5 =</p>

  <p>new DataInputStream(</p>

  <p>new BufferedInputStream(</p>

  <p>new FileInputStream("Data.txt")));</p>

  <p>BufferedReader in5br =</p>

  <p>new BufferedReader(</p>

  <p>new InputStreamReader(in5));</p>

  <p>// Must use DataInputStream for data:</p>

  <p>System.out.println(in5.readDouble());</p>

  <p>// Can now use the "proper" readLine():</p>

  <p>System.out.println(in5br.readLine());</p>

  <p>} catch(EOFException e) {</p>

  <p>System.out.println("End of stream");</p>

  <p>}</p>

  <p>// 6. Reading and writing random access</p>

  <p>// files is the same as before.</p>

  <p>// (not repeated here)</p>

  <p>} catch(FileNotFoundException e) {</p>

  <p>System.out.println(</p>

  <p>"File Not Found:" + args[1]);</p>

  <p>} catch(IOException e) {</p>

  <p>System.out.println("IO Exception");</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>大家一般看见的是转换过程非常直观，代码看起来也颇相似。但这些都不是重要的区别。最重要的是，由于随机访问文件已经改变，所以第6节未再重复。</p>

  <p>第1节收缩了一点儿，因为假如要做的全部事情就是读取行输入，那么只需要将一个FileReader封装到BufferedReader之内即可。第1b节展示了封装System.in，以便读取控制台输入的新方法。这里的代码量增多了一些，因为System.in是一个DataInputStream，而且BufferedReader需要一个Reader参数，所以要用InputStreamReader来进行转换。</p>

  <p>在2节，可以看到如果有一个字串，而且想从中读取数据，只需用一个StringReader替换StringBufferInputStream，剩下的代码是完全相同的。</p>

  <p>第3节揭示了新IO流库设计中的一个错误。如果有一个字串，而且想从中读取数据，那么不能再以任何形式使用StringBufferInputStream。若编译一个涉及StringBufferInputStream的代码，会得到一条“反对”消息，告诉我们不要用它。此时最好换用一个StringReader。但是，假如要象第3节这样进行格式化的内存输入，就必须使用DataInputStream――没有什么“DataReader”可以代替它――而DataInputStream很不幸地要求用到一个InputStream参数。所以我们没有选择的余地，只好使用编译器不赞成的StringBufferInputStream类。编译器同样会发出反对信息，但我们对此束手无策（注释②）。</p>

  <p>StringReader替换StringBufferInputStream，剩下的代码是完全相同的。</p>

  <p>②：到你现在正式使用的时候，这个错误可能已经修正。</p>

  <p>第4节明显是从老式数据流到新数据流的一个直接转换，没有需要特别指出的。在第5节中，我们被强迫使用所有的老式数据流，因为DataOutputStream和DataInputStream要求用到它们，而且没有可供替换的东西。然而，编译期间不会产生任何“反对”信息。若不赞成一种数据流，通常是由于它的构建器产生了一条反对消息，禁止我们使用整个类。但在DataInputStream的情况下，只有readLine()是不赞成使用的，因为我们最好为readLine()使用一个BufferedReader（但为其他所有格式化输入都使用一个DataInputStream）。</p>

  <p>若比较第5节和IOStreamDemo.java中的那一小节，会注意到在这个版本中，数据是在文本之前写入的。那是由于Java 1.1本身存在一个错误，如下述代码所示：</p>

  <p>//: IOBug.java</p>

  <p>// Java 1.1 (and higher?) IO Bug</p>

  <p>import java.io.*;</p>

  <p>public class IOBug {</p>

  <p>public static void main(String[] args)</p>

  <p>throws Exception {</p>

  <p>DataOutputStream out =</p>

  <p>new DataOutputStream(</p>

  <p>new BufferedOutputStream(</p>

  <p>new FileOutputStream("Data.txt")));</p>

  <p>out.writeDouble(3.14159);</p>

  <p>out.writeBytes("That was the value of pi\n");</p>

  <p>out.writeBytes("This is pi/2:\n");</p>

  <p>out.writeDouble(3.14159/2);</p>

  <p>out.close();</p>

  <p>DataInputStream in =</p>

  <p>new DataInputStream(</p>

  <p>new BufferedInputStream(</p>

  <p>new FileInputStream("Data.txt")));</p>

  <p>BufferedReader inbr =</p>

  <p>new BufferedReader(</p>

  <p>new InputStreamReader(in));</p>

  <p>// The doubles written BEFORE the line of text</p>

  <p>// read back correctly:</p>

  <p>System.out.println(in.readDouble());</p>

  <p>// Read the lines of text:</p>

  <p>System.out.println(inbr.readLine());</p>

  <p>System.out.println(inbr.readLine());</p>

  <p>// Trying to read the doubles after the line</p>

  <p>// produces an end-of-file exception:</p>

  <p>System.out.println(in.readDouble());</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>看起来，我们在对一个writeBytes()的调用之后写入的任何东西都不是能够恢复的。这是一个十分有限的错误，希望在你读到本书的时候已获得改正。为检测是否改正，请运行上述程序。若没有得到一个违例，而且值都能正确打印出来，就表明已经改正。</p>

  <p>10.7.5 重导向标准IO</p>

  <p>Java 1.1在System类中添加了特殊的方法，允许我们重新定向标准输入、输出以及错误IO流。此时要用到下述简单的静态方法调用：</p>

  <p>setIn(InputStream)</p>

  <p>setOut(PrintStream)</p>

  <p>setErr(PrintStream)</p>

  <p>如果突然要在屏幕上生成大量输出，而且滚动的速度快于人们的阅读速度，输出的重定向就显得特别有用。在一个命令行程序中，如果想重复测试一个特定的用户输入序列，输入的重定向也显得特别有价值。下面这个简单的例子展示了这些方法的使用：</p>

  <p>//: Redirecting.java</p>

  <p>// Demonstrates the use of redirection for</p>

  <p>// standard IO in Java 1.1</p>

  <p>import java.io.*;</p>

  <p>class Redirecting {</p>

  <p>public static void main(String[] args) {</p>

  <p>try {</p>

  <p>BufferedInputStream in =</p>

  <p>new BufferedInputStream(</p>

  <p>new FileInputStream(</p>

  <p>"Redirecting.java"));</p>

  <p>// Produces deprecation message:</p>

  <p>PrintStream out =</p>

  <p>new PrintStream(</p>

  <p>new BufferedOutputStream(</p>

  <p>new FileOutputStream("test.out")));</p>

  <p>System.setIn(in);</p>

  <p>System.setOut(out);</p>

  <p>System.setErr(out);</p>

  <p>BufferedReader br =</p>

  <p>new BufferedReader(</p>

  <p>new InputStreamReader(System.in));</p>

  <p>String s;</p>

  <p>while((s = br.readLine()) != null)</p>

  <p>System.out.println(s);</p>

  <p>out.close(); // Remember this!</p>

  <p>} catch(IOException e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>这个程序的作用是将标准输入同一个文件连接起来，并将标准输出和错误重定向至另一个文件。</p>

  <p>这是不可避免会遇到“反对”消息的另一个例子。用-deprecation标志编译时得到的消息如下：</p>

  <p>Note:The constructor java.io.PrintStream(java.io.OutputStream) has been deprecated.</p>

  <p>注意：不推荐使用构建器java.io.PrintStream（java.io.OutputStream）。</p>

  <p>然而，无论System.setOut()还是System.setErr()都要求用一个PrintStream作为参数使用，所以必须调用PrintStream构建器。所以大家可能会觉得奇怪，既然Java 1.1通过反对构建器而反对了整个PrintStream，为什么库的设计人员在添加这个反对的同时，依然为System添加了新方法，且指明要求用PrintStream，而不是用PrintWriter呢？毕竟，后者是一个崭新和首选的替换措施呀？这真令人费解。</p>

  <p>10.8 压缩</p>

  <p>Java 1.1也添加一个类，用以支持对压缩格式的数据流的读写。它们封装到现成的IO类中，以提供压缩功能。</p>

  <p>此时Java 1.1的一个问题显得非常突出：它们不是从新的Reader和Writer类衍生出来的，而是属于InputStream和OutputStream层次结构的一部分。所以有时不得不混合使用两种类型的数据流（注意可用InputStreamReader和OutputStreamWriter在不同的类型间方便地进行转换）。</p>

  <p>Java 1.1压缩类 功能</p>

  <p>CheckedInputStream GetCheckSum()为任何InputStream产生校验和（不仅是解压）</p>

  <p>CheckedOutputStream GetCheckSum()为任何OutputStream产生校验和（不仅是解压）</p>

  <p>DeflaterOutputStream 用于压缩类的基础类</p>

  <p>ZipOutputStream 一个DeflaterOutputStream，将数据压缩成Zip文件格式</p>

  <p>GZIPOutputStream 一个DeflaterOutputStream，将数据压缩成GZIP文件格式</p>

  <p>InflaterInputStream 用于解压类的基础类</p>

  <p>ZipInputStream 一个DeflaterInputStream，解压用Zip文件格式保存的数据</p>

  <p>GZIPInputStream 一个DeflaterInputStream，解压用GZIP文件格式保存的数据</p>

  <p>尽管存在许多种压缩算法，但是Zip和GZIP可能最常用的。所以能够很方便地用多种现成的工具来读写这些格式的压缩数据。</p>

  <p>10.8.1 用GZIP进行简单压缩</p>

  <p>GZIP接口非常简单，所以如果只有单个数据流需要压缩（而不是一系列不同的数据），那么它就可能是最适当选择。下面是对单个文件进行压缩的例子：</p>

  <p>//: GZIPcompress.java</p>

  <p>// Uses Java 1.1 GZIP compression to compress</p>

  <p>// a file whose name is passed on the command</p>

  <p>// line.</p>

  <p>import java.io.*;</p>

  <p>import java.util.zip.*;</p>

  <p>public class GZIPcompress {</p>

  <p>public static void main(String[] args) {</p>

  <p>try {</p>

  <p>BufferedReader in =</p>

  <p>new BufferedReader(</p>

  <p>new FileReader(args[0]));</p>

  <p>BufferedOutputStream out =</p>

  <p>new BufferedOutputStream(</p>

  <p>new GZIPOutputStream(</p>

  <p>new FileOutputStream("test.gz")));</p>

  <p>System.out.println("Writing file");</p>

  <p>int c;</p>

  <p>while((c = in.read()) != -1)</p>

  <p>out.write(c);</p>

  <p>in.close();</p>

  <p>out.close();</p>

  <p>System.out.println("Reading file");</p>

  <p>BufferedReader in2 =</p>

  <p>new BufferedReader(</p>

  <p>new InputStreamReader(</p>

  <p>new GZIPInputStream(</p>

  <p>new FileInputStream("test.gz"))));</p>

  <p>String s;</p>

  <p>while((s = in2.readLine()) != null)</p>

  <p>System.out.println(s);</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>压缩类的用法非常直观――只需将输出流封装到一个GZIPOutputStream或者ZipOutputStream内，并将输入流封装到GZIPInputStream或者ZipInputStream内即可。剩余的全部操作就是标准的IO读写。然而，这是一个很典型的例子，我们不得不混合使用新旧IO流：数据的输入使用Reader类，而GZIPOutputStream的构建器只能接收一个OutputStream对象，不能接收Writer对象。</p>

  <p>10.8.2 用Zip进行多文件保存</p>

  <p>提供了Zip支持的Java 1.1库显得更加全面。利用它可以方便地保存多个文件。甚至有一个独立的类来简化对Zip文件的读操作。这个库采采用的是标准Zip格式，所以能与当前因特网上使用的大量压缩、解压工具很好地协作。下面这个例子采取了与前例相同的形式，但能根据我们需要控制任意数量的命令行参数。除此之外，它展示了如何用Checksum类来计算和校验文件的“校验和”（Checksum）。可选用两种类型的Checksum：Adler32（速度要快一些）和CRC32（慢一些，但更准确）。</p>

  <p>//: ZipCompress.java</p>

  <p>// Uses Java 1.1 Zip compression to compress</p>

  <p>// any number of files whose names are passed</p>

  <p>// on the command line.</p>

  <p>import java.io.*;</p>

  <p>import java.util.*;</p>

  <p>import java.util.zip.*;</p>

  <p>public class ZipCompress {</p>

  <p>public static void main(String[] args) {</p>

  <p>try {</p>

  <p>FileOutputStream f =</p>

  <p>new FileOutputStream("test.zip");</p>

  <p>CheckedOutputStream csum =</p>

  <p>new CheckedOutputStream(</p>

  <p>f, new Adler32());</p>

  <p>ZipOutputStream out =</p>

  <p>new ZipOutputStream(</p>

  <p>new BufferedOutputStream(csum));</p>

  <p>out.setComment("A test of Java Zipping");</p>

  <p>// Can't read the above comment, though</p>

  <p>for(int i = 0; i &lt; args.length; i++) {</p>

  <p>System.out.println(</p>

  <p>"Writing file " + args[i]);</p>

  <p>BufferedReader in =</p>

  <p>new BufferedReader(</p>

  <p>new FileReader(args[i]));</p>

  <p>out.putNextEntry(new ZipEntry(args[i]));</p>

  <p>int c;</p>

  <p>while((c = in.read()) != -1)</p>

  <p>out.write(c);</p>

  <p>in.close();</p>

  <p>}</p>

  <p>out.close();</p>

  <p>// Checksum valid only after the file</p>

  <p>// has been closed!</p>

  <p>System.out.println("Checksum: " +</p>

  <p>csum.getChecksum().getValue());</p>

  <p>// Now extract the files:</p>

  <p>System.out.println("Reading file");</p>

  <p>FileInputStream fi =</p>

  <p>new FileInputStream("test.zip");</p>

  <p>CheckedInputStream csumi =</p>

  <p>new CheckedInputStream(</p>

  <p>fi, new Adler32());</p>

  <p>ZipInputStream in2 =</p>

  <p>new ZipInputStream(</p>

  <p>new BufferedInputStream(csumi));</p>

  <p>ZipEntry ze;</p>

  <p>System.out.println("Checksum: " +</p>

  <p>csumi.getChecksum().getValue());</p>

  <p>while((ze = in2.getNextEntry()) != null) {</p>

  <p>System.out.println("Reading file " + ze);</p>

  <p>int x;</p>

  <p>while((x = in2.read()) != -1)</p>

  <p>System.out.write(x);</p>

  <p>}</p>

  <p>in2.close();</p>

  <p>// Alternative way to open and read</p>

  <p>// zip files:</p>

  <p>ZipFile zf = new ZipFile("test.zip");</p>

  <p>Enumeration e = zf.entries();</p>

  <p>while(e.hasMoreElements()) {</p>

  <p>ZipEntry ze2 = (ZipEntry)e.nextElement();</p>

  <p>System.out.println("File: " + ze2);</p>

  <p>// ... and extract the data as before</p>

  <p>}</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>对于要加入压缩档的每一个文件，都必须调用putNextEntry()，并将其传递给一个ZipEntry对象。ZipEntry对象包含了一个功能全面的接口，利用它可以获取和设置Zip文件内那个特定的Entry（入口）上能够接受的所有数据：名字、压缩后和压缩前的长度、日期、CRC校验和、额外字段的数据、注释、压缩方法以及它是否一个目录入口等等。然而，虽然Zip格式提供了设置密码的方法，但Java的Zip库没有提供这方面的支持。而且尽管CheckedInputStream和CheckedOutputStream同时提供了对Adler32和CRC32校验和的支持，但是ZipEntry只支持CRC的接口。这虽然属于基层Zip格式的限制，但却限制了我们使用速度更快的Adler32。</p>

  <p>为解压文件，ZipInputStream提供了一个getNextEntry()方法，能在有的前提下返回下一个ZipEntry。作为一个更简洁的方法，可以用ZipFile对象读取文件。该对象有一个entries()方法，可以为ZipEntry返回一个Enumeration（枚举）。</p>

  <p>为读取校验和，必须多少拥有对关联的Checksum对象的访问权限。在这里保留了指向CheckedOutputStream和CheckedInputStream对象的一个句柄。但是，也可以只占有指向Checksum对象的一个句柄。</p>

  <p>Zip流中一个令人困惑的方法是setComment()。正如前面展示的那样，我们可在写一个文件时设置注释内容，但却没有办法取出ZipInputStream内的注释。看起来，似乎只能通过ZipEntry逐个入口地提供对注释的完全支持。</p>

  <p>当然，使用GZIP或Zip库时并不仅仅限于文件――可以压缩任何东西，包括要通过网络连接发送的数据。</p>

  <p>10.8.3 Java归档（jar）实用程序</p>

  <p>Zip格式亦在Java 1.1的JAR（Java ARchive）文件格式中得到了采用。这种文件格式的作用是将一系列文件合并到单个压缩文件里，就象Zip那样。然而，同Java中其他任何东西一样，JAR文件是跨平台的，所以不必关心涉及具体平台的问题。除了可以包括声音和图像文件以外，也可以在其中包括类文件。</p>

  <p>涉及因特网应用时，JAR文件显得特别有用。在JAR文件之前，Web浏览器必须重复多次请求Web服务器，以便下载完构成一个“程序片”（Applet）的所有文件。除此以外，每个文件都是未经压缩的。但在将所有这些文件合并到一个JAR文件里以后，只需向远程服务器发出一次请求即可。同时，由于采用了压缩技术，所以可在更短的时间里获得全部数据。另外，JAR文件里的每个入口（条目）都可以加上数字化签名（详情参考Java用户文档）。</p>

  <p>一个JAR文件由一系列采用Zip压缩格式的文件构成，同时还有一张“详情单”，对所有这些文件进行了描述（可创建自己的详情单文件；否则，jar程序会为我们代劳）。在联机用户文档中，可以找到与JAR详情单更多的资料（详情单的英语是“Manifest”）。</p>

  <p>jar实用程序已与Sun的JDK配套提供，可以按我们的选择自动压缩文件。请在命令行调用它：</p>

  <p>jar [选项] 说明 [详情单] 输入文件</p>

  <p>其中，“选项”用一系列字母表示（不必输入连字号或其他任何指示符）。如下所示：</p>

  <p>c 创建新的或空的压缩档</p>

  <p>t 列出目录表</p>

  <p>x 解压所有文件</p>

  <p>x file 解压指定文件</p>

  <p>f 指出“我准备向你提供文件名”。若省略此参数，jar会假定它的输入来自标准输入；或者在它创建文件时，输出会进入标准输出内</p>

  <p>m 指出第一个参数将是用户自建的详情表文件的名字</p>

  <p>v 产生详细输出，对jar做的工作进行巨细无遗的描述</p>

  <p>O 只保存文件；不压缩文件（用于创建一个JAR文件，以便我们将其置入自己的类路径中）</p>

  <p>M 不自动生成详情表文件</p>

  <p>在准备进入JAR文件的文件中，若包括了一个子目录，那个子目录会自动添加，其中包括它自己的所有子目录，以此类推。路径信息也会得到保留。</p>

  <p>下面是调用jar的一些典型方法：</p>

  <p>jar cf myJarFile.jar *.class</p>

  <p>用于创建一个名为myJarFile.jar的JAR文件，其中包含了当前目录中的所有类文件，同时还有自动产生的详情表文件。</p>

  <p>jar cmf myJarFile.jar myManifestFile.mf *.class</p>

  <p>与前例类似，但添加了一个名为myManifestFile.mf的用户自建详情表文件。</p>

  <p>jar tf myJarFile.jar</p>

  <p>生成myJarFile.jar内所有文件的一个目录表。</p>

  <p>jar tvf myJarFile.jar</p>

  <p>添加“verbose”（详尽）标志，提供与myJarFile.jar中的文件有关的、更详细的资料。</p>

  <p>jar cvf myApp.jar audio classes image</p>

  <p>假定audio，classes和image是子目录，这样便将所有子目录合并到文件myApp.jar中。其中也包括了“verbose”标志，可在jar程序工作时反馈更详尽的信息。</p>

  <p>如果用O选项创建了一个JAR文件，那个文件就可置入自己的类路径（CLASSPATH）中：</p>

  <p>CLASSPATH="lib1.jar;lib2.jar;"</p>

  <p>Java能在lib1.jar和lib2.jar中搜索目标类文件。</p>

  <p>jar工具的功能没有zip工具那么丰富。例如，不能够添加或更新一个现成JAR文件中的文件，只能从头开始新建一个JAR文件。此外，不能将文件移入一个JAR文件，并在移动后将它们删除。然而，在一种平台上创建的JAR文件可在其他任何平台上由jar工具毫无阻碍地读出（这个问题有时会困扰zip工具）。</p>

  <p>正如大家在第13章会看到的那样，我们也用JAR为Java Beans打包。</p>

  <p>10.9 对象序列化</p>

  <p>Java 1.1增添了一种有趣的特性，名为“对象序列化”（Object Serialization）。它面向那些实现了Serializable接口的对象，可将它们转换成一系列字节，并可在以后完全恢复回原来的样子。这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。换句话说，可以先在Windows机器上创建一个对象，对其序列化，然后通过网络发给一台Unix机器，然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示，也不必关心字节的顺序或者其他任何细节。</p>

  <p>就其本身来说，对象的序列化是非常有趣的，因为利用它可以实现“有限持久化”。请记住“持久化”意味着对象的“生存时间”并不取决于程序是否正在执行――它存在或“生存”于程序的每一次调用之间。通过序列化一个对象，将其写入磁盘，以后在程序重新调用时重新恢复那个对象，就能圆满实现一种“持久”效果。之所以称其为“有限”，是因为不能用某种“persistent”（持久）关键字简单地地定义一个对象，并让系统自动照看其他所有细节问题（尽管将来可能成为现实）。相反，必须在自己的程序中明确地序列化和组装对象。</p>

  <p>语言里增加了对象序列化的概念后，可提供对两种主要特性的支持。Java 1.1的“远程方法调用”（RMI）使本来存在于其他机器的对象可以表现出好象就在本地机器上的行为。将消息发给远程对象时，需要通过对象序列化来传输参数和返回值。RMI将在第15章作具体讨论。</p>

  <p>对象的序列化也是Java Beans必需的，后者由Java 1.1引入。使用一个Bean时，它的状态信息通常在设计期间配置好。程序启动以后，这种状态信息必须保存下来，以便程序启动以后恢复；具体工作由对象序列化完成。</p>

  <p>对象的序列化处理非常简单，只需对象实现了Serializable接口即可（该接口仅是一个标记，没有方法）。在Java 1.1中，许多标准库类都发生了改变，以便能够序列化――其中包括用于基本数据类型的全部封装器、所有集合类以及其他许多东西。甚至Class对象也可以序列化（第11章讲述了具体实现过程）。</p>

  <p>为序列化一个对象，首先要创建某些OutputStream对象，然后将其封装到ObjectOutputStream对象内。此时，只需调用writeObject()即可完成对象的序列化，并将其发送给OutputStream。相反的过程是将一个InputStream封装到ObjectInputStream内，然后调用readObject()。和往常一样，我们最后获得的是指向一个上溯造型Object的句柄，所以必须下溯造型，以便能够直接设置。</p>

  <p>对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”，而且能追踪对象内包含的所有句柄并保存那些对象；接着又能对每个对象内包含的句柄进行追踪；以此类推。我们有时将这种情况称为“对象网”，单个对象可与之建立连接。而且它还包含了对象的句柄数组以及成员对象。若必须自行操纵一套对象序列化机制，那么在代码里追踪所有这些链接时可能会显得非常麻烦。在另一方面，由于Java对象的序列化似乎找不出什么缺点，所以请尽量不要自己动手，让它用优化的算法自动维护整个对象网。下面这个例子对序列化机制进行了测试。它建立了许多链接对象的一个“Worm”（蠕虫），每个对象都与Worm中的下一段链接，同时又与属于不同类（Data）的对象句柄数组链接：</p>

  <p>//: Worm.java</p>

  <p>// Demonstrates object serialization in Java 1.1</p>

  <p>import java.io.*;</p>

  <p>class Data implements Serializable {</p>

  <p>private int i;</p>

  <p>Data(int x) { i = x; }</p>

  <p>public String toString() {</p>

  <p>return Integer.toString(i);</p>

  <p>}</p>

  <p>}</p>

  <p>public class Worm implements Serializable {</p>

  <p>// Generate a random int value:</p>

  <p>private static int r() {</p>

  <p>return (int)(Math.random() * 10);</p>

  <p>}</p>

  <p>private Data[] d = {</p>

  <p>new Data(r()), new Data(r()), new Data(r())</p>

  <p>};</p>

  <p>private Worm next;</p>

  <p>private char c;</p>

  <p>// Value of i == number of segments</p>

  <p>Worm(int i, char x) {</p>

  <p>System.out.println(" Worm constructor: " + i);</p>

  <p>c = x;</p>

  <p>if(--i &gt; 0)</p>

  <p>next = new Worm(i, (char)(x + 1));</p>

  <p>}</p>

  <p>Worm() {</p>

  <p>System.out.println("Default constructor");</p>

  <p>}</p>

  <p>public String toString() {</p>

  <p>String s = ":" + c + "(";</p>

  <p>for(int i = 0; i &lt; d.length; i++)</p>

  <p>s += d[i].toString();</p>

  <p>s += ")";</p>

  <p>if(next != null)</p>

  <p>s += next.toString();</p>

  <p>return s;</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>Worm w = new Worm(6, 'a');</p>

  <p>System.out.println("w = " + w);</p>

  <p>try {</p>

  <p>ObjectOutputStream out =</p>

  <p>new ObjectOutputStream(</p>

  <p>new FileOutputStream("worm.out"));</p>

  <p>out.writeObject("Worm storage");</p>

  <p>out.writeObject(w);</p>

  <p>out.close(); // Also flushes output</p>

  <p>ObjectInputStream in =</p>

  <p>new ObjectInputStream(</p>

  <p>new FileInputStream("worm.out"));</p>

  <p>String s = (String)in.readObject();</p>

  <p>Worm w2 = (Worm)in.readObject();</p>

  <p>System.out.println(s + ", w2 = " + w2);</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>try {</p>

  <p>ByteArrayOutputStream bout =</p>

  <p>new ByteArrayOutputStream();</p>

  <p>ObjectOutputStream out =</p>

  <p>new ObjectOutputStream(bout);</p>

  <p>out.writeObject("Worm storage");</p>

  <p>out.writeObject(w);</p>

  <p>out.flush();</p>

  <p>ObjectInputStream in =</p>

  <p>new ObjectInputStream(</p>

  <p>new ByteArrayInputStream(</p>

  <p>bout.toByteArray()));</p>

  <p>String s = (String)in.readObject();</p>

  <p>Worm w3 = (Worm)in.readObject();</p>

  <p>System.out.println(s + ", w3 = " + w3);</p>

  <p>} catch(Exception e) {</p>

  <p>e.printStackTrace();</p>

  <p>}</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>更有趣的是，Worm内的Data对象数组是用随机数字初始化的（这样便不用怀疑编译器保留了某种原始信息）。每个Worm段都用一个Char标记。这个Char是在重复生成链接的Worm列表时自动产生的。创建一个Worm时，需告诉构建器希望它有多长。为产生下一个句柄（next），它总是用减去1的长度来调用Worm构建器。最后一个next句柄则保持为null（空），表示已抵达Worm的尾部。</p>

  <p>上面的所有操作都是为了加深事情的复杂程度，加大对象序列化的难度。然而，真正的序列化过程却是非常简单的。一旦从另外某个流里创建了ObjectOutputStream，writeObject()就会序列化对象。注意也可以为一个String调用writeObject()。亦可使用与DataOutputStream相同的方法写入所有基本数据类型（它们有相同的接口）。</p>

  <p>有两个单独的try块看起来是类似的。第一个读写的是文件，而另一个读写的是一个ByteArray（字节数组）。可利用对任何DataInputStream或者DataOutputStream的序列化来读写特定的对象；正如在关于连网的那一章会讲到的那样，这些对象甚至包括网络。一次循环后的输出结果如下：</p>

  <p>Worm constructor: 6</p>

  <p>Worm constructor: 5</p>

  <p>Worm c</p>

  <div class="mbppagebreak"></div>
</body>
</html>
